/*
 * Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.imageio.plugins.jpeg;

import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.plugins.jpeg.JPEGQTable;
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.ListIterator;
import java.io.IOException;
import java.awt.color.ICC_Profile;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.Point;

/**
 * Metadata for the JPEG plug-in.
 */
public class JPEGMetadata extends IIOMetadata implements Cloneable {

  //////// Private variables

  private static final boolean debug = false;

  /**
   * A copy of <code>markerSequence</code>, created the first time the
   * <code>markerSequence</code> is modified.  This is used by reset
   * to restore the original state.
   */
  private List resetSequence = null;

  /**
   * Set to <code>true</code> when reading a thumbnail stored as
   * JPEG.  This is used to enforce the prohibition of JFIF thumbnails
   * containing any JFIF marker segments, and to ensure generation of
   * a correct native subtree during <code>getAsTree</code>.
   */
  private boolean inThumb = false;

  /**
   * Set by the chroma node construction method to signal the
   * presence or absence of an alpha channel to the transparency
   * node construction method.  Used only when constructing a
   * standard metadata tree.
   */
  private boolean hasAlpha;

  //////// end of private variables

  /////// Package-access variables

  /**
   * All data is a list of <code>MarkerSegment</code> objects.
   * When accessing the list, use the tag to identify the particular
   * subclass.  Any JFIF marker segment must be the first element
   * of the list if it is present, and any JFXX or APP2ICC marker
   * segments are subordinate to the JFIF marker segment.  This
   * list is package visible so that the writer can access it.
   *
   * @see #MarkerSegment
   */
  List markerSequence = new ArrayList();

  /**
   * Indicates whether this object represents stream or image
   * metadata.  Package-visible so the writer can see it.
   */
  final boolean isStream;

  /////// End of package-access variables

  /////// Constructors

  /**
   * Constructor containing code shared by other constructors.
   */
  JPEGMetadata(boolean isStream, boolean inThumb) {
    super(true,  // Supports standard format
        JPEG.nativeImageMetadataFormatName,  // and a native format
        JPEG.nativeImageMetadataFormatClassName,
        null, null);  // No other formats
    this.inThumb = inThumb;
    // But if we are stream metadata, adjust the variables
    this.isStream = isStream;
    if (isStream) {
      nativeMetadataFormatName = JPEG.nativeStreamMetadataFormatName;
      nativeMetadataFormatClassName =
          JPEG.nativeStreamMetadataFormatClassName;
    }
  }

  /*
   * Constructs a <code>JPEGMetadata</code> object by reading the
   * contents of an <code>ImageInputStream</code>.  Has package-only
   * access.
   *
   * @param isStream A boolean indicating whether this object will be
   * stream or image metadata.
   * @param isThumb A boolean indicating whether this metadata object
   * is for an image or for a thumbnail stored as JPEG.
   * @param iis An <code>ImageInputStream</code> from which to read
   * the metadata.
   * @param reader The <code>JPEGImageReader</code> calling this
   * constructor, to which warnings should be sent.
   */
  JPEGMetadata(boolean isStream,
      boolean isThumb,
      ImageInputStream iis,
      JPEGImageReader reader) throws IOException {
    this(isStream, isThumb);

    JPEGBuffer buffer = new JPEGBuffer(iis);

    buffer.loadBuf(0);

    // The first three bytes should be FF, SOI, FF
    if (((buffer.buf[0] & 0xff) != 0xff)
        || ((buffer.buf[1] & 0xff) != JPEG.SOI)
        || ((buffer.buf[2] & 0xff) != 0xff)) {
      throw new IIOException("Image format error");
    }

    boolean done = false;
    buffer.bufAvail -= 2;  // Next byte should be the ff before a marker
    buffer.bufPtr = 2;
    MarkerSegment newGuy = null;
    while (!done) {
      byte[] buf;
      int ptr;
      buffer.loadBuf(1);
      if (debug) {
        System.out.println("top of loop");
        buffer.print(10);
      }
      buffer.scanForFF(reader);
      switch (buffer.buf[buffer.bufPtr] & 0xff) {
        case 0:
          if (debug) {
            System.out.println("Skipping 0");
          }
          buffer.bufAvail--;
          buffer.bufPtr++;
          break;
        case JPEG.SOF0:
        case JPEG.SOF1:
        case JPEG.SOF2:
          if (isStream) {
            throw new IIOException
                ("SOF not permitted in stream metadata");
          }
          newGuy = new SOFMarkerSegment(buffer);
          break;
        case JPEG.DQT:
          newGuy = new DQTMarkerSegment(buffer);
          break;
        case JPEG.DHT:
          newGuy = new DHTMarkerSegment(buffer);
          break;
        case JPEG.DRI:
          newGuy = new DRIMarkerSegment(buffer);
          break;
        case JPEG.APP0:
          // Either JFIF, JFXX, or unknown APP0
          buffer.loadBuf(8); // tag, length, id
          buf = buffer.buf;
          ptr = buffer.bufPtr;
          if ((buf[ptr + 3] == 'J')
              && (buf[ptr + 4] == 'F')
              && (buf[ptr + 5] == 'I')
              && (buf[ptr + 6] == 'F')
              && (buf[ptr + 7] == 0)) {
            if (inThumb) {
              reader.warningOccurred
                  (JPEGImageReader.WARNING_NO_JFIF_IN_THUMB);
              // Leave newGuy null
              // Read a dummy to skip the segment
              JFIFMarkerSegment dummy =
                  new JFIFMarkerSegment(buffer);
            } else if (isStream) {
              throw new IIOException
                  ("JFIF not permitted in stream metadata");
            } else if (markerSequence.isEmpty() == false) {
              throw new IIOException
                  ("JFIF APP0 must be first marker after SOI");
            } else {
              newGuy = new JFIFMarkerSegment(buffer);
            }
          } else if ((buf[ptr + 3] == 'J')
              && (buf[ptr + 4] == 'F')
              && (buf[ptr + 5] == 'X')
              && (buf[ptr + 6] == 'X')
              && (buf[ptr + 7] == 0)) {
            if (isStream) {
              throw new IIOException
                  ("JFXX not permitted in stream metadata");
            }
            if (inThumb) {
              throw new IIOException
                  ("JFXX markers not allowed in JFIF JPEG thumbnail");
            }
            JFIFMarkerSegment jfif =
                (JFIFMarkerSegment) findMarkerSegment
                    (JFIFMarkerSegment.class, true);
            if (jfif == null) {
              throw new IIOException
                  ("JFXX encountered without prior JFIF!");
            }
            jfif.addJFXX(buffer, reader);
            // newGuy remains null
          } else {
            newGuy = new MarkerSegment(buffer);
            newGuy.loadData(buffer);
          }
          break;
        case JPEG.APP2:
          // Either an ICC profile or unknown APP2
          buffer.loadBuf(15); // tag, length, id
          if ((buffer.buf[buffer.bufPtr + 3] == 'I')
              && (buffer.buf[buffer.bufPtr + 4] == 'C')
              && (buffer.buf[buffer.bufPtr + 5] == 'C')
              && (buffer.buf[buffer.bufPtr + 6] == '_')
              && (buffer.buf[buffer.bufPtr + 7] == 'P')
              && (buffer.buf[buffer.bufPtr + 8] == 'R')
              && (buffer.buf[buffer.bufPtr + 9] == 'O')
              && (buffer.buf[buffer.bufPtr + 10] == 'F')
              && (buffer.buf[buffer.bufPtr + 11] == 'I')
              && (buffer.buf[buffer.bufPtr + 12] == 'L')
              && (buffer.buf[buffer.bufPtr + 13] == 'E')
              && (buffer.buf[buffer.bufPtr + 14] == 0)
              ) {
            if (isStream) {
              throw new IIOException
                  ("ICC profiles not permitted in stream metadata");
            }

            JFIFMarkerSegment jfif =
                (JFIFMarkerSegment) findMarkerSegment
                    (JFIFMarkerSegment.class, true);
            if (jfif == null) {
              newGuy = new MarkerSegment(buffer);
              newGuy.loadData(buffer);
            } else {
              jfif.addICC(buffer);
            }
            // newGuy remains null
          } else {
            newGuy = new MarkerSegment(buffer);
            newGuy.loadData(buffer);
          }
          break;
        case JPEG.APP14:
          // Either Adobe or unknown APP14
          buffer.loadBuf(8); // tag, length, id
          if ((buffer.buf[buffer.bufPtr + 3] == 'A')
              && (buffer.buf[buffer.bufPtr + 4] == 'd')
              && (buffer.buf[buffer.bufPtr + 5] == 'o')
              && (buffer.buf[buffer.bufPtr + 6] == 'b')
              && (buffer.buf[buffer.bufPtr + 7] == 'e')) {
            if (isStream) {
              throw new IIOException
                  ("Adobe APP14 markers not permitted in stream metadata");
            }
            newGuy = new AdobeMarkerSegment(buffer);
          } else {
            newGuy = new MarkerSegment(buffer);
            newGuy.loadData(buffer);
          }

          break;
        case JPEG.COM:
          newGuy = new COMMarkerSegment(buffer);
          break;
        case JPEG.SOS:
          if (isStream) {
            throw new IIOException
                ("SOS not permitted in stream metadata");
          }
          newGuy = new SOSMarkerSegment(buffer);
          break;
        case JPEG.RST0:
        case JPEG.RST1:
        case JPEG.RST2:
        case JPEG.RST3:
        case JPEG.RST4:
        case JPEG.RST5:
        case JPEG.RST6:
        case JPEG.RST7:
          if (debug) {
            System.out.println("Restart Marker");
          }
          buffer.bufPtr++; // Just skip it
          buffer.bufAvail--;
          break;
        case JPEG.EOI:
          done = true;
          buffer.bufPtr++;
          buffer.bufAvail--;
          break;
        default:
          newGuy = new MarkerSegment(buffer);
          newGuy.loadData(buffer);
          newGuy.unknown = true;
          break;
      }
      if (newGuy != null) {
        markerSequence.add(newGuy);
        if (debug) {
          newGuy.print();
        }
        newGuy = null;
      }
    }

    // Now that we've read up to the EOI, we need to push back
    // whatever is left in the buffer, so that the next read
    // in the native code will work.

    buffer.pushBack();

    if (!isConsistent()) {
      throw new IIOException("Inconsistent metadata read from stream");
    }
  }

  /**
   * Constructs a default stream <code>JPEGMetadata</code> object appropriate
   * for the given write parameters.
   */
  JPEGMetadata(ImageWriteParam param, JPEGImageWriter writer) {
    this(true, false);

    JPEGImageWriteParam jparam = null;

    if ((param != null) && (param instanceof JPEGImageWriteParam)) {
      jparam = (JPEGImageWriteParam) param;
      if (!jparam.areTablesSet()) {
        jparam = null;
      }
    }
    if (jparam != null) {
      markerSequence.add(new DQTMarkerSegment(jparam.getQTables()));
      markerSequence.add
          (new DHTMarkerSegment(jparam.getDCHuffmanTables(),
              jparam.getACHuffmanTables()));
    } else {
      // default tables.
      markerSequence.add(new DQTMarkerSegment(JPEG.getDefaultQTables()));
      markerSequence.add(new DHTMarkerSegment(JPEG.getDefaultHuffmanTables(true),
          JPEG.getDefaultHuffmanTables(false)));
    }

    // Defensive programming
    if (!isConsistent()) {
      throw new InternalError("Default stream metadata is inconsistent");
    }
  }

  /**
   * Constructs a default image <code>JPEGMetadata</code> object appropriate
   * for the given image type and write parameters.
   */
  JPEGMetadata(ImageTypeSpecifier imageType,
      ImageWriteParam param,
      JPEGImageWriter writer) {
    this(false, false);

    boolean wantJFIF = true;
    boolean wantAdobe = false;
    int transform = JPEG.ADOBE_UNKNOWN;
    boolean willSubsample = true;
    boolean wantICC = false;
    boolean wantProg = false;
    boolean wantOptimized = false;
    boolean wantExtended = false;
    boolean wantQTables = true;
    boolean wantHTables = true;
    float quality = JPEG.DEFAULT_QUALITY;
    byte[] componentIDs = {1, 2, 3, 4};
    int numComponents = 0;

    ImageTypeSpecifier destType = null;

    if (param != null) {
      destType = param.getDestinationType();
      if (destType != null) {
        if (imageType != null) {
          // Ignore the destination type.
          writer.warningOccurred
              (JPEGImageWriter.WARNING_DEST_IGNORED);
          destType = null;
        }
      }
      // The only progressive mode that makes sense here is MODE_DEFAULT
      if (param.canWriteProgressive()) {
        // the param may not be one of ours, so it may return false.
        // If so, the following would throw an exception
        if (param.getProgressiveMode() == ImageWriteParam.MODE_DEFAULT) {
          wantProg = true;
          wantOptimized = true;
          wantHTables = false;
        }
      }

      if (param instanceof JPEGImageWriteParam) {
        JPEGImageWriteParam jparam = (JPEGImageWriteParam) param;
        if (jparam.areTablesSet()) {
          wantQTables = false;  // If the param has them, metadata shouldn't
          wantHTables = false;
          if ((jparam.getDCHuffmanTables().length > 2)
              || (jparam.getACHuffmanTables().length > 2)) {
            wantExtended = true;
          }
        }
        // Progressive forces optimized, regardless of param setting
        // so consult the param re optimized only if not progressive
        if (!wantProg) {
          wantOptimized = jparam.getOptimizeHuffmanTables();
          if (wantOptimized) {
            wantHTables = false;
          }
        }
      }

      // compression quality should determine the q tables.  Note that this
      // will be ignored if we already decided not to create any.
      // Again, the param may not be one of ours, so we must check that it
      // supports compression settings
      if (param.canWriteCompressed()) {
        if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
          quality = param.getCompressionQuality();
        }
      }
    }

    // We are done with the param, now for the image types

    ColorSpace cs = null;
    if (destType != null) {
      ColorModel cm = destType.getColorModel();
      numComponents = cm.getNumComponents();
      boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents);
      boolean hasAlpha = cm.hasAlpha();
      cs = cm.getColorSpace();
      int type = cs.getType();
      switch (type) {
        case ColorSpace.TYPE_GRAY:
          willSubsample = false;
          if (hasExtraComponents) {  // e.g. alpha
            wantJFIF = false;
          }
          break;
        case ColorSpace.TYPE_3CLR:
          if (cs == JPEG.JCS.getYCC()) {
            wantJFIF = false;
            componentIDs[0] = (byte) 'Y';
            componentIDs[1] = (byte) 'C';
            componentIDs[2] = (byte) 'c';
            if (hasAlpha) {
              componentIDs[3] = (byte) 'A';
            }
          }
          break;
        case ColorSpace.TYPE_YCbCr:
          if (hasExtraComponents) { // e.g. K or alpha
            wantJFIF = false;
            if (!hasAlpha) { // Not alpha, so must be K
              wantAdobe = true;
              transform = JPEG.ADOBE_YCCK;
            }
          }
          break;
        case ColorSpace.TYPE_RGB:  // with or without alpha
          wantJFIF = false;
          wantAdobe = true;
          willSubsample = false;
          componentIDs[0] = (byte) 'R';
          componentIDs[1] = (byte) 'G';
          componentIDs[2] = (byte) 'B';
          if (hasAlpha) {
            componentIDs[3] = (byte) 'A';
          }
          break;
        default:
          // Everything else is not subsampled, gets no special marker,
          // and component ids are 1 - N
          wantJFIF = false;
          willSubsample = false;
      }
    } else if (imageType != null) {
      ColorModel cm = imageType.getColorModel();
      numComponents = cm.getNumComponents();
      boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents);
      boolean hasAlpha = cm.hasAlpha();
      cs = cm.getColorSpace();
      int type = cs.getType();
      switch (type) {
        case ColorSpace.TYPE_GRAY:
          willSubsample = false;
          if (hasExtraComponents) {  // e.g. alpha
            wantJFIF = false;
          }
          break;
        case ColorSpace.TYPE_RGB:  // with or without alpha
          // without alpha we just accept the JFIF defaults
          if (hasAlpha) {
            wantJFIF = false;
          }
          break;
        case ColorSpace.TYPE_3CLR:
          wantJFIF = false;
          willSubsample = false;
          if (cs.equals(ColorSpace.getInstance(ColorSpace.CS_PYCC))) {
            willSubsample = true;
            wantAdobe = true;
            componentIDs[0] = (byte) 'Y';
            componentIDs[1] = (byte) 'C';
            componentIDs[2] = (byte) 'c';
            if (hasAlpha) {
              componentIDs[3] = (byte) 'A';
            }
          }
          break;
        case ColorSpace.TYPE_YCbCr:
          if (hasExtraComponents) { // e.g. K or alpha
            wantJFIF = false;
            if (!hasAlpha) {  // then it must be K
              wantAdobe = true;
              transform = JPEG.ADOBE_YCCK;
            }
          }
          break;
        case ColorSpace.TYPE_CMYK:
          wantJFIF = false;
          wantAdobe = true;
          transform = JPEG.ADOBE_YCCK;
          break;

        default:
          // Everything else is not subsampled, gets no special marker,
          // and component ids are 0 - N
          wantJFIF = false;
          willSubsample = false;
      }

    }

    // do we want an ICC profile?
    if (wantJFIF && JPEG.isNonStandardICC(cs)) {
      wantICC = true;
    }

    // Now step through the markers, consulting our variables.
    if (wantJFIF) {
      JFIFMarkerSegment jfif = new JFIFMarkerSegment();
      markerSequence.add(jfif);
      if (wantICC) {
        try {
          jfif.addICC((ICC_ColorSpace) cs);
        } catch (IOException e) {
        } // Can't happen here
      }
    }
    // Adobe
    if (wantAdobe) {
      markerSequence.add(new AdobeMarkerSegment(transform));
    }

    // dqt
    if (wantQTables) {
      markerSequence.add(new DQTMarkerSegment(quality, willSubsample));
    }

    // dht
    if (wantHTables) {
      markerSequence.add(new DHTMarkerSegment(willSubsample));
    }

    // sof
    markerSequence.add(new SOFMarkerSegment(wantProg,
        wantExtended,
        willSubsample,
        componentIDs,
        numComponents));

    // sos
    if (!wantProg) {  // Default progression scans are done in the writer
      markerSequence.add(new SOSMarkerSegment(willSubsample,
          componentIDs,
          numComponents));
    }

    // Defensive programming
    if (!isConsistent()) {
      throw new InternalError("Default image metadata is inconsistent");
    }
  }

  ////// End of constructors

  // Utilities for dealing with the marker sequence.
  // The first ones have package access for access from the writer.

  /**
   * Returns the first MarkerSegment object in the list
   * with the given tag, or null if none is found.
   */
  MarkerSegment findMarkerSegment(int tag) {
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg.tag == tag) {
        return seg;
      }
    }
    return null;
  }

  /**
   * Returns the first or last MarkerSegment object in the list
   * of the given class, or null if none is found.
   */
  MarkerSegment findMarkerSegment(Class cls, boolean first) {
    if (first) {
      Iterator iter = markerSequence.iterator();
      while (iter.hasNext()) {
        MarkerSegment seg = (MarkerSegment) iter.next();
        if (cls.isInstance(seg)) {
          return seg;
        }
      }
    } else {
      ListIterator iter = markerSequence.listIterator(markerSequence.size());
      while (iter.hasPrevious()) {
        MarkerSegment seg = (MarkerSegment) iter.previous();
        if (cls.isInstance(seg)) {
          return seg;
        }
      }
    }
    return null;
  }

  /**
   * Returns the index of the first or last MarkerSegment in the list
   * of the given class, or -1 if none is found.
   */
  private int findMarkerSegmentPosition(Class cls, boolean first) {
    if (first) {
      ListIterator iter = markerSequence.listIterator();
      for (int i = 0; iter.hasNext(); i++) {
        MarkerSegment seg = (MarkerSegment) iter.next();
        if (cls.isInstance(seg)) {
          return i;
        }
      }
    } else {
      ListIterator iter = markerSequence.listIterator(markerSequence.size());
      for (int i = markerSequence.size() - 1; iter.hasPrevious(); i--) {
        MarkerSegment seg = (MarkerSegment) iter.previous();
        if (cls.isInstance(seg)) {
          return i;
        }
      }
    }
    return -1;
  }

  private int findLastUnknownMarkerSegmentPosition() {
    ListIterator iter = markerSequence.listIterator(markerSequence.size());
    for (int i = markerSequence.size() - 1; iter.hasPrevious(); i--) {
      MarkerSegment seg = (MarkerSegment) iter.previous();
      if (seg.unknown == true) {
        return i;
      }
    }
    return -1;
  }

  // Implement Cloneable, but restrict access

  protected Object clone() {
    JPEGMetadata newGuy = null;
    try {
      newGuy = (JPEGMetadata) super.clone();
    } catch (CloneNotSupportedException e) {
    } // won't happen
    if (markerSequence != null) {
      newGuy.markerSequence = (List) cloneSequence();
    }
    newGuy.resetSequence = null;
    return newGuy;
  }

  /**
   * Returns a deep copy of the current marker sequence.
   */
  private List cloneSequence() {
    if (markerSequence == null) {
      return null;
    }
    List retval = new ArrayList(markerSequence.size());
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      retval.add(seg.clone());
    }

    return retval;
  }

  // Tree methods

  public Node getAsTree(String formatName) {
    if (formatName == null) {
      throw new IllegalArgumentException("null formatName!");
    }
    if (isStream) {
      if (formatName.equals(JPEG.nativeStreamMetadataFormatName)) {
        return getNativeTree();
      }
    } else {
      if (formatName.equals(JPEG.nativeImageMetadataFormatName)) {
        return getNativeTree();
      }
      if (formatName.equals
          (IIOMetadataFormatImpl.standardMetadataFormatName)) {
        return getStandardTree();
      }
    }
    throw new IllegalArgumentException("Unsupported format name: "
        + formatName);
  }

  IIOMetadataNode getNativeTree() {
    IIOMetadataNode root;
    IIOMetadataNode top;
    Iterator iter = markerSequence.iterator();
    if (isStream) {
      root = new IIOMetadataNode(JPEG.nativeStreamMetadataFormatName);
      top = root;
    } else {
      IIOMetadataNode sequence = new IIOMetadataNode("markerSequence");
      if (!inThumb) {
        root = new IIOMetadataNode(JPEG.nativeImageMetadataFormatName);
        IIOMetadataNode header = new IIOMetadataNode("JPEGvariety");
        root.appendChild(header);
        JFIFMarkerSegment jfif = (JFIFMarkerSegment)
            findMarkerSegment(JFIFMarkerSegment.class, true);
        if (jfif != null) {
          iter.next();  // JFIF must be first, so this skips it
          header.appendChild(jfif.getNativeNode());
        }
        root.appendChild(sequence);
      } else {
        root = sequence;
      }
      top = sequence;
    }
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      top.appendChild(seg.getNativeNode());
    }
    return root;
  }

  // Standard tree node methods

  protected IIOMetadataNode getStandardChromaNode() {
    hasAlpha = false;  // Unless we find otherwise

    // Colorspace type - follow the rules in the spec
    // First get the SOF marker segment, if there is one
    SOFMarkerSegment sof = (SOFMarkerSegment)
        findMarkerSegment(SOFMarkerSegment.class, true);
    if (sof == null) {
      // No image, so no chroma
      return null;
    }

    IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
    IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
    chroma.appendChild(csType);

    // get the number of channels
    int numChannels = sof.componentSpecs.length;

    IIOMetadataNode numChanNode = new IIOMetadataNode("NumChannels");
    chroma.appendChild(numChanNode);
    numChanNode.setAttribute("value", Integer.toString(numChannels));

    // is there a JFIF marker segment?
    if (findMarkerSegment(JFIFMarkerSegment.class, true) != null) {
      if (numChannels == 1) {
        csType.setAttribute("name", "GRAY");
      } else {
        csType.setAttribute("name", "YCbCr");
      }
      return chroma;
    }

    // How about an Adobe marker segment?
    AdobeMarkerSegment adobe =
        (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true);
    if (adobe != null) {
      switch (adobe.transform) {
        case JPEG.ADOBE_YCCK:
          csType.setAttribute("name", "YCCK");
          break;
        case JPEG.ADOBE_YCC:
          csType.setAttribute("name", "YCbCr");
          break;
        case JPEG.ADOBE_UNKNOWN:
          if (numChannels == 3) {
            csType.setAttribute("name", "RGB");
          } else if (numChannels == 4) {
            csType.setAttribute("name", "CMYK");
          }
          break;
      }
      return chroma;
    }

    // Neither marker.  Check components
    if (numChannels < 3) {
      csType.setAttribute("name", "GRAY");
      if (numChannels == 2) {
        hasAlpha = true;
      }
      return chroma;
    }

    boolean idsAreJFIF = true;

    for (int i = 0; i < sof.componentSpecs.length; i++) {
      int id = sof.componentSpecs[i].componentId;
      if ((id < 1) || (id >= sof.componentSpecs.length)) {
        idsAreJFIF = false;
      }
    }

    if (idsAreJFIF) {
      csType.setAttribute("name", "YCbCr");
      if (numChannels == 4) {
        hasAlpha = true;
      }
      return chroma;
    }

    // Check against the letters
    if ((sof.componentSpecs[0].componentId == 'R')
        && (sof.componentSpecs[1].componentId == 'G')
        && (sof.componentSpecs[2].componentId == 'B')) {

      csType.setAttribute("name", "RGB");
      if ((numChannels == 4)
          && (sof.componentSpecs[3].componentId == 'A')) {
        hasAlpha = true;
      }
      return chroma;
    }

    if ((sof.componentSpecs[0].componentId == 'Y')
        && (sof.componentSpecs[1].componentId == 'C')
        && (sof.componentSpecs[2].componentId == 'c')) {

      csType.setAttribute("name", "PhotoYCC");
      if ((numChannels == 4)
          && (sof.componentSpecs[3].componentId == 'A')) {
        hasAlpha = true;
      }
      return chroma;
    }

    // Finally, 3-channel subsampled are YCbCr, unsubsampled are RGB
    // 4-channel subsampled are YCbCrA, unsubsampled are CMYK

    boolean subsampled = false;

    int hfactor = sof.componentSpecs[0].HsamplingFactor;
    int vfactor = sof.componentSpecs[0].VsamplingFactor;

    for (int i = 1; i < sof.componentSpecs.length; i++) {
      if ((sof.componentSpecs[i].HsamplingFactor != hfactor)
          || (sof.componentSpecs[i].VsamplingFactor != vfactor)) {
        subsampled = true;
        break;
      }
    }

    if (subsampled) {
      csType.setAttribute("name", "YCbCr");
      if (numChannels == 4) {
        hasAlpha = true;
      }
      return chroma;
    }

    // Not subsampled.  numChannels < 3 is taken care of above
    if (numChannels == 3) {
      csType.setAttribute("name", "RGB");
    } else {
      csType.setAttribute("name", "CMYK");
    }

    return chroma;
  }

  protected IIOMetadataNode getStandardCompressionNode() {

    IIOMetadataNode compression = new IIOMetadataNode("Compression");

    // CompressionTypeName
    IIOMetadataNode name = new IIOMetadataNode("CompressionTypeName");
    name.setAttribute("value", "JPEG");
    compression.appendChild(name);

    // Lossless - false
    IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
    lossless.setAttribute("value", "FALSE");
    compression.appendChild(lossless);

    // NumProgressiveScans - count sos segments
    int sosCount = 0;
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment ms = (MarkerSegment) iter.next();
      if (ms.tag == JPEG.SOS) {
        sosCount++;
      }
    }
    if (sosCount != 0) {
      IIOMetadataNode prog = new IIOMetadataNode("NumProgressiveScans");
      prog.setAttribute("value", Integer.toString(sosCount));
      compression.appendChild(prog);
    }

    return compression;
  }

  protected IIOMetadataNode getStandardDimensionNode() {
    // If we have a JFIF marker segment, we know a little
    // otherwise all we know is the orientation, which is always normal
    IIOMetadataNode dim = new IIOMetadataNode("Dimension");
    IIOMetadataNode orient = new IIOMetadataNode("ImageOrientation");
    orient.setAttribute("value", "normal");
    dim.appendChild(orient);

    JFIFMarkerSegment jfif =
        (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
    if (jfif != null) {

      // Aspect Ratio is width of pixel / height of pixel
      float aspectRatio;
      if (jfif.resUnits == 0) {
        // In this case they just encode aspect ratio directly
        aspectRatio = ((float) jfif.Xdensity) / jfif.Ydensity;
      } else {
        // They are true densities (e.g. dpi) and must be inverted
        aspectRatio = ((float) jfif.Ydensity) / jfif.Xdensity;
      }
      IIOMetadataNode aspect = new IIOMetadataNode("PixelAspectRatio");
      aspect.setAttribute("value", Float.toString(aspectRatio));
      dim.insertBefore(aspect, orient);

      // Pixel size
      if (jfif.resUnits != 0) {
        // 1 == dpi, 2 == dpc
        float scale = (jfif.resUnits == 1) ? 25.4F : 10.0F;

        IIOMetadataNode horiz =
            new IIOMetadataNode("HorizontalPixelSize");
        horiz.setAttribute("value",
            Float.toString(scale / jfif.Xdensity));
        dim.appendChild(horiz);

        IIOMetadataNode vert =
            new IIOMetadataNode("VerticalPixelSize");
        vert.setAttribute("value",
            Float.toString(scale / jfif.Ydensity));
        dim.appendChild(vert);
      }
    }
    return dim;
  }

  protected IIOMetadataNode getStandardTextNode() {
    IIOMetadataNode text = null;
    // Add a text entry for each COM Marker Segment
    if (findMarkerSegment(JPEG.COM) != null) {
      text = new IIOMetadataNode("Text");
      Iterator iter = markerSequence.iterator();
      while (iter.hasNext()) {
        MarkerSegment seg = (MarkerSegment) iter.next();
        if (seg.tag == JPEG.COM) {
          COMMarkerSegment com = (COMMarkerSegment) seg;
          IIOMetadataNode entry = new IIOMetadataNode("TextEntry");
          entry.setAttribute("keyword", "comment");
          entry.setAttribute("value", com.getComment());
          text.appendChild(entry);
        }
      }
    }
    return text;
  }

  protected IIOMetadataNode getStandardTransparencyNode() {
    IIOMetadataNode trans = null;
    if (hasAlpha == true) {
      trans = new IIOMetadataNode("Transparency");
      IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
      alpha.setAttribute("value", "nonpremultiplied"); // Always assume
      trans.appendChild(alpha);
    }
    return trans;
  }

  // Editing

  public boolean isReadOnly() {
    return false;
  }

  public void mergeTree(String formatName, Node root)
      throws IIOInvalidTreeException {
    if (formatName == null) {
      throw new IllegalArgumentException("null formatName!");
    }
    if (root == null) {
      throw new IllegalArgumentException("null root!");
    }
    List copy = null;
    if (resetSequence == null) {
      resetSequence = cloneSequence();  // Deep copy
      copy = resetSequence;  // Avoid cloning twice
    } else {
      copy = cloneSequence();
    }
    if (isStream &&
        (formatName.equals(JPEG.nativeStreamMetadataFormatName))) {
      mergeNativeTree(root);
    } else if (!isStream &&
        (formatName.equals(JPEG.nativeImageMetadataFormatName))) {
      mergeNativeTree(root);
    } else if (!isStream &&
        (formatName.equals
            (IIOMetadataFormatImpl.standardMetadataFormatName))) {
      mergeStandardTree(root);
    } else {
      throw new IllegalArgumentException("Unsupported format name: "
          + formatName);
    }
    if (!isConsistent()) {
      markerSequence = copy;
      throw new IIOInvalidTreeException
          ("Merged tree is invalid; original restored", root);
    }
  }

  private void mergeNativeTree(Node root) throws IIOInvalidTreeException {
    String name = root.getNodeName();
    if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName
        : JPEG.nativeImageMetadataFormatName)) {
      throw new IIOInvalidTreeException("Invalid root node name: " + name,
          root);
    }
    if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence
      throw new IIOInvalidTreeException(
          "JPEGvariety and markerSequence nodes must be present", root);
    }
    mergeJFIFsubtree(root.getFirstChild());
    mergeSequenceSubtree(root.getLastChild());
  }

  /**
   * Merge a JFIF subtree into the marker sequence, if the subtree
   * is non-empty.
   * If a JFIF marker exists, update it from the subtree.
   * If none exists, create one from the subtree and insert it at the
   * beginning of the marker sequence.
   */
  private void mergeJFIFsubtree(Node JPEGvariety)
      throws IIOInvalidTreeException {
    if (JPEGvariety.getChildNodes().getLength() != 0) {
      Node jfifNode = JPEGvariety.getFirstChild();
      // is there already a jfif marker segment?
      JFIFMarkerSegment jfifSeg =
          (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
      if (jfifSeg != null) {
        jfifSeg.updateFromNativeNode(jfifNode, false);
      } else {
        // Add it as the first element in the list.
        markerSequence.add(0, new JFIFMarkerSegment(jfifNode));
      }
    }
  }

  private void mergeSequenceSubtree(Node sequenceTree)
      throws IIOInvalidTreeException {
    NodeList children = sequenceTree.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node node = children.item(i);
      String name = node.getNodeName();
      if (name.equals("dqt")) {
        mergeDQTNode(node);
      } else if (name.equals("dht")) {
        mergeDHTNode(node);
      } else if (name.equals("dri")) {
        mergeDRINode(node);
      } else if (name.equals("com")) {
        mergeCOMNode(node);
      } else if (name.equals("app14Adobe")) {
        mergeAdobeNode(node);
      } else if (name.equals("unknown")) {
        mergeUnknownNode(node);
      } else if (name.equals("sof")) {
        mergeSOFNode(node);
      } else if (name.equals("sos")) {
        mergeSOSNode(node);
      } else {
        throw new IIOInvalidTreeException("Invalid node: " + name, node);
      }
    }
  }

  /**
   * Merge the given DQT node into the marker sequence.  If there already
   * exist DQT marker segments in the sequence, then each table in the
   * node replaces the first table, in any DQT segment, with the same
   * table id.  If none of the existing DQT segments contain a table with
   * the same id, then the table is added to the last existing DQT segment.
   * If there are no DQT segments, then a new one is created and added
   * as follows:
   * If there are DHT segments, the new DQT segment is inserted before the
   * first one.
   * If there are no DHT segments, the new DQT segment is inserted before
   * an SOF segment, if there is one.
   * If there is no SOF segment, the new DQT segment is inserted before
   * the first SOS segment, if there is one.
   * If there is no SOS segment, the new DQT segment is added to the end
   * of the sequence.
   */
  private void mergeDQTNode(Node node) throws IIOInvalidTreeException {
    // First collect any existing DQT nodes into a local list
    ArrayList oldDQTs = new ArrayList();
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof DQTMarkerSegment) {
        oldDQTs.add(seg);
      }
    }
    if (!oldDQTs.isEmpty()) {
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        int childID = MarkerSegment.getAttributeValue(child,
            null,
            "qtableId",
            0, 3,
            true);
        DQTMarkerSegment dqt = null;
        int tableIndex = -1;
        for (int j = 0; j < oldDQTs.size(); j++) {
          DQTMarkerSegment testDQT = (DQTMarkerSegment) oldDQTs.get(j);
          for (int k = 0; k < testDQT.tables.size(); k++) {
            DQTMarkerSegment.Qtable testTable =
                (DQTMarkerSegment.Qtable) testDQT.tables.get(k);
            if (childID == testTable.tableID) {
              dqt = testDQT;
              tableIndex = k;
              break;
            }
          }
          if (dqt != null) {
            break;
          }
        }
        if (dqt != null) {
          dqt.tables.set(tableIndex, dqt.getQtableFromNode(child));
        } else {
          dqt = (DQTMarkerSegment) oldDQTs.get(oldDQTs.size() - 1);
          dqt.tables.add(dqt.getQtableFromNode(child));
        }
      }
    } else {
      DQTMarkerSegment newGuy = new DQTMarkerSegment(node);
      int firstDHT = findMarkerSegmentPosition(DHTMarkerSegment.class, true);
      int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true);
      int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
      if (firstDHT != -1) {
        markerSequence.add(firstDHT, newGuy);
      } else if (firstSOF != -1) {
        markerSequence.add(firstSOF, newGuy);
      } else if (firstSOS != -1) {
        markerSequence.add(firstSOS, newGuy);
      } else {
        markerSequence.add(newGuy);
      }
    }
  }

  /**
   * Merge the given DHT node into the marker sequence.  If there already
   * exist DHT marker segments in the sequence, then each table in the
   * node replaces the first table, in any DHT segment, with the same
   * table class and table id.  If none of the existing DHT segments contain
   * a table with the same class and id, then the table is added to the last
   * existing DHT segment.
   * If there are no DHT segments, then a new one is created and added
   * as follows:
   * If there are DQT segments, the new DHT segment is inserted immediately
   * following the last DQT segment.
   * If there are no DQT segments, the new DHT segment is inserted before
   * an SOF segment, if there is one.
   * If there is no SOF segment, the new DHT segment is inserted before
   * the first SOS segment, if there is one.
   * If there is no SOS segment, the new DHT segment is added to the end
   * of the sequence.
   */
  private void mergeDHTNode(Node node) throws IIOInvalidTreeException {
    // First collect any existing DQT nodes into a local list
    ArrayList oldDHTs = new ArrayList();
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof DHTMarkerSegment) {
        oldDHTs.add(seg);
      }
    }
    if (!oldDHTs.isEmpty()) {
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        NamedNodeMap attrs = child.getAttributes();
        int childID = MarkerSegment.getAttributeValue(child,
            attrs,
            "htableId",
            0, 3,
            true);
        int childClass = MarkerSegment.getAttributeValue(child,
            attrs,
            "class",
            0, 1,
            true);
        DHTMarkerSegment dht = null;
        int tableIndex = -1;
        for (int j = 0; j < oldDHTs.size(); j++) {
          DHTMarkerSegment testDHT = (DHTMarkerSegment) oldDHTs.get(j);
          for (int k = 0; k < testDHT.tables.size(); k++) {
            DHTMarkerSegment.Htable testTable =
                (DHTMarkerSegment.Htable) testDHT.tables.get(k);
            if ((childID == testTable.tableID) &&
                (childClass == testTable.tableClass)) {
              dht = testDHT;
              tableIndex = k;
              break;
            }
          }
          if (dht != null) {
            break;
          }
        }
        if (dht != null) {
          dht.tables.set(tableIndex, dht.getHtableFromNode(child));
        } else {
          dht = (DHTMarkerSegment) oldDHTs.get(oldDHTs.size() - 1);
          dht.tables.add(dht.getHtableFromNode(child));
        }
      }
    } else {
      DHTMarkerSegment newGuy = new DHTMarkerSegment(node);
      int lastDQT = findMarkerSegmentPosition(DQTMarkerSegment.class, false);
      int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true);
      int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
      if (lastDQT != -1) {
        markerSequence.add(lastDQT + 1, newGuy);
      } else if (firstSOF != -1) {
        markerSequence.add(firstSOF, newGuy);
      } else if (firstSOS != -1) {
        markerSequence.add(firstSOS, newGuy);
      } else {
        markerSequence.add(newGuy);
      }
    }
  }

  /**
   * Merge the given DRI node into the marker sequence.
   * If there already exists a DRI marker segment, the restart interval
   * value is updated.
   * If there is no DRI segment, then a new one is created and added as
   * follows:
   * If there is an SOF segment, the new DRI segment is inserted before
   * it.
   * If there is no SOF segment, the new DRI segment is inserted before
   * the first SOS segment, if there is one.
   * If there is no SOS segment, the new DRI segment is added to the end
   * of the sequence.
   */
  private void mergeDRINode(Node node) throws IIOInvalidTreeException {
    DRIMarkerSegment dri =
        (DRIMarkerSegment) findMarkerSegment(DRIMarkerSegment.class, true);
    if (dri != null) {
      dri.updateFromNativeNode(node, false);
    } else {
      DRIMarkerSegment newGuy = new DRIMarkerSegment(node);
      int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true);
      int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
      if (firstSOF != -1) {
        markerSequence.add(firstSOF, newGuy);
      } else if (firstSOS != -1) {
        markerSequence.add(firstSOS, newGuy);
      } else {
        markerSequence.add(newGuy);
      }
    }
  }

  /**
   * Merge the given COM node into the marker sequence.
   * A new COM marker segment is created and added to the sequence
   * using insertCOMMarkerSegment.
   */
  private void mergeCOMNode(Node node) throws IIOInvalidTreeException {
    COMMarkerSegment newGuy = new COMMarkerSegment(node);
    insertCOMMarkerSegment(newGuy);
  }

  /**
   * Insert a new COM marker segment into an appropriate place in the
   * marker sequence, as follows:
   * If there already exist COM marker segments, the new one is inserted
   * after the last one.
   * If there are no COM segments, the new COM segment is inserted after the
   * JFIF segment, if there is one.
   * If there is no JFIF segment, the new COM segment is inserted after the
   * Adobe marker segment, if there is one.
   * If there is no Adobe segment, the new COM segment is inserted
   * at the beginning of the sequence.
   */
  private void insertCOMMarkerSegment(COMMarkerSegment newGuy) {
    int lastCOM = findMarkerSegmentPosition(COMMarkerSegment.class, false);
    boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null);
    int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true);
    if (lastCOM != -1) {
      markerSequence.add(lastCOM + 1, newGuy);
    } else if (hasJFIF) {
      markerSequence.add(1, newGuy);  // JFIF is always 0
    } else if (firstAdobe != -1) {
      markerSequence.add(firstAdobe + 1, newGuy);
    } else {
      markerSequence.add(0, newGuy);
    }
  }

  /**
   * Merge the given Adobe APP14 node into the marker sequence.
   * If there already exists an Adobe marker segment, then its attributes
   * are updated from the node.
   * If there is no Adobe segment, then a new one is created and added
   * using insertAdobeMarkerSegment.
   */
  private void mergeAdobeNode(Node node) throws IIOInvalidTreeException {
    AdobeMarkerSegment adobe =
        (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true);
    if (adobe != null) {
      adobe.updateFromNativeNode(node, false);
    } else {
      AdobeMarkerSegment newGuy = new AdobeMarkerSegment(node);
      insertAdobeMarkerSegment(newGuy);
    }
  }

  /**
   * Insert the given AdobeMarkerSegment into the marker sequence, as
   * follows (we assume there is no Adobe segment yet):
   * If there is a JFIF segment, then the new Adobe segment is inserted
   * after it.
   * If there is no JFIF segment, the new Adobe segment is inserted after the
   * last Unknown segment, if there are any.
   * If there are no Unknown segments, the new Adobe segment is inserted
   * at the beginning of the sequence.
   */
  private void insertAdobeMarkerSegment(AdobeMarkerSegment newGuy) {
    boolean hasJFIF =
        (findMarkerSegment(JFIFMarkerSegment.class, true) != null);
    int lastUnknown = findLastUnknownMarkerSegmentPosition();
    if (hasJFIF) {
      markerSequence.add(1, newGuy);  // JFIF is always 0
    } else if (lastUnknown != -1) {
      markerSequence.add(lastUnknown + 1, newGuy);
    } else {
      markerSequence.add(0, newGuy);
    }
  }

  /**
   * Merge the given Unknown node into the marker sequence.
   * A new Unknown marker segment is created and added to the sequence as
   * follows:
   * If there already exist Unknown marker segments, the new one is inserted
   * after the last one.
   * If there are no Unknown marker segments, the new Unknown marker segment
   * is inserted after the JFIF segment, if there is one.
   * If there is no JFIF segment, the new Unknown segment is inserted before
   * the Adobe marker segment, if there is one.
   * If there is no Adobe segment, the new Unknown segment is inserted
   * at the beginning of the sequence.
   */
  private void mergeUnknownNode(Node node) throws IIOInvalidTreeException {
    MarkerSegment newGuy = new MarkerSegment(node);
    int lastUnknown = findLastUnknownMarkerSegmentPosition();
    boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null);
    int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true);
    if (lastUnknown != -1) {
      markerSequence.add(lastUnknown + 1, newGuy);
    } else if (hasJFIF) {
      markerSequence.add(1, newGuy);  // JFIF is always 0
    }
    if (firstAdobe != -1) {
      markerSequence.add(firstAdobe, newGuy);
    } else {
      markerSequence.add(0, newGuy);
    }
  }

  /**
   * Merge the given SOF node into the marker sequence.
   * If there already exists an SOF marker segment in the sequence, then
   * its values are updated from the node.
   * If there is no SOF segment, then a new one is created and added as
   * follows:
   * If there are any SOS segments, the new SOF segment is inserted before
   * the first one.
   * If there is no SOS segment, the new SOF segment is added to the end
   * of the sequence.
   */
  private void mergeSOFNode(Node node) throws IIOInvalidTreeException {
    SOFMarkerSegment sof =
        (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true);
    if (sof != null) {
      sof.updateFromNativeNode(node, false);
    } else {
      SOFMarkerSegment newGuy = new SOFMarkerSegment(node);
      int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
      if (firstSOS != -1) {
        markerSequence.add(firstSOS, newGuy);
      } else {
        markerSequence.add(newGuy);
      }
    }
  }

  /**
   * Merge the given SOS node into the marker sequence.
   * If there already exists a single SOS marker segment, then the values
   * are updated from the node.
   * If there are more than one existing SOS marker segments, then an
   * IIOInvalidTreeException is thrown, as SOS segments cannot be merged
   * into a set of progressive scans.
   * If there are no SOS marker segments, a new one is created and added
   * to the end of the sequence.
   */
  private void mergeSOSNode(Node node) throws IIOInvalidTreeException {
    SOSMarkerSegment firstSOS =
        (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true);
    SOSMarkerSegment lastSOS =
        (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, false);
    if (firstSOS != null) {
      if (firstSOS != lastSOS) {
        throw new IIOInvalidTreeException
            ("Can't merge SOS node into a tree with > 1 SOS node", node);
      }
      firstSOS.updateFromNativeNode(node, false);
    } else {
      markerSequence.add(new SOSMarkerSegment(node));
    }
  }

  private boolean transparencyDone;

  private void mergeStandardTree(Node root) throws IIOInvalidTreeException {
    transparencyDone = false;
    NodeList children = root.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node node = children.item(i);
      String name = node.getNodeName();
      if (name.equals("Chroma")) {
        mergeStandardChromaNode(node, children);
      } else if (name.equals("Compression")) {
        mergeStandardCompressionNode(node);
      } else if (name.equals("Data")) {
        mergeStandardDataNode(node);
      } else if (name.equals("Dimension")) {
        mergeStandardDimensionNode(node);
      } else if (name.equals("Document")) {
        mergeStandardDocumentNode(node);
      } else if (name.equals("Text")) {
        mergeStandardTextNode(node);
      } else if (name.equals("Transparency")) {
        mergeStandardTransparencyNode(node);
      } else {
        throw new IIOInvalidTreeException("Invalid node: " + name, node);
      }
    }
  }

    /*
     * In general, it could be possible to convert all non-pixel data to some
     * textual form and include it in comments, but then this would create the
     * expectation that these comment forms be recognized by the reader, thus
     * creating a defacto extension to JPEG metadata capabilities.  This is
     * probably best avoided, so the following convert only text nodes to
     * comments, and lose the keywords as well.
     */

  private void mergeStandardChromaNode(Node node, NodeList siblings)
      throws IIOInvalidTreeException {
    // ColorSpaceType can change the target colorspace for compression
    // This must take any transparency node into account as well, as
    // that affects the number of channels (if alpha is present).  If
    // a transparency node is dealt with here, set a flag to indicate
    // this to the transparency processor below.  If we discover that
    // the nodes are not in order, throw an exception as the tree is
    // invalid.

    if (transparencyDone) {
      throw new IIOInvalidTreeException
          ("Transparency node must follow Chroma node", node);
    }

    Node csType = node.getFirstChild();
    if ((csType == null) || !csType.getNodeName().equals("ColorSpaceType")) {
      // If there is no ColorSpaceType node, we have nothing to do
      return;
    }

    String csName = csType.getAttributes().getNamedItem("name").getNodeValue();

    int numChannels = 0;
    boolean wantJFIF = false;
    boolean wantAdobe = false;
    int transform = 0;
    boolean willSubsample = false;
    byte[] ids = {1, 2, 3, 4};  // JFIF compatible
    if (csName.equals("GRAY")) {
      numChannels = 1;
      wantJFIF = true;
    } else if (csName.equals("YCbCr")) {
      numChannels = 3;
      wantJFIF = true;
      willSubsample = true;
    } else if (csName.equals("PhotoYCC")) {
      numChannels = 3;
      wantAdobe = true;
      transform = JPEG.ADOBE_YCC;
      ids[0] = (byte) 'Y';
      ids[1] = (byte) 'C';
      ids[2] = (byte) 'c';
    } else if (csName.equals("RGB")) {
      numChannels = 3;
      wantAdobe = true;
      transform = JPEG.ADOBE_UNKNOWN;
      ids[0] = (byte) 'R';
      ids[1] = (byte) 'G';
      ids[2] = (byte) 'B';
    } else if ((csName.equals("XYZ"))
        || (csName.equals("Lab"))
        || (csName.equals("Luv"))
        || (csName.equals("YxY"))
        || (csName.equals("HSV"))
        || (csName.equals("HLS"))
        || (csName.equals("CMY"))
        || (csName.equals("3CLR"))) {
      numChannels = 3;
    } else if (csName.equals("YCCK")) {
      numChannels = 4;
      wantAdobe = true;
      transform = JPEG.ADOBE_YCCK;
      willSubsample = true;
    } else if (csName.equals("CMYK")) {
      numChannels = 4;
      wantAdobe = true;
      transform = JPEG.ADOBE_UNKNOWN;
    } else if (csName.equals("4CLR")) {
      numChannels = 4;
    } else { // We can't handle them, so don't modify any metadata
      return;
    }

    boolean wantAlpha = false;
    for (int i = 0; i < siblings.getLength(); i++) {
      Node trans = siblings.item(i);
      if (trans.getNodeName().equals("Transparency")) {
        wantAlpha = wantAlpha(trans);
        break;  // out of for
      }
    }

    if (wantAlpha) {
      numChannels++;
      wantJFIF = false;
      if (ids[0] == (byte) 'R') {
        ids[3] = (byte) 'A';
        wantAdobe = false;
      }
    }

    JFIFMarkerSegment jfif =
        (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
    AdobeMarkerSegment adobe =
        (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true);
    SOFMarkerSegment sof =
        (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true);
    SOSMarkerSegment sos =
        (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true);

    // If the metadata specifies progressive, then the number of channels
    // must match, so that we can modify all the existing SOS marker segments.
    // If they don't match, we don't know what to do with SOS so we can't do
    // the merge.  We then just return silently.
    // An exception would not be appropriate.  A warning might, but we have
    // nowhere to send it to.
    if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive
      if ((sof.componentSpecs.length != numChannels) && (sos != null)) {
        return;
      }
    }

    // JFIF header might be removed
    if (!wantJFIF && (jfif != null)) {
      markerSequence.remove(jfif);
    }

    // Now add a JFIF if we do want one, but only if it isn't stream metadata
    if (wantJFIF && !isStream) {
      markerSequence.add(0, new JFIFMarkerSegment());
    }

    // Adobe header might be removed or the transform modified, if it isn't
    // stream metadata
    if (wantAdobe) {
      if ((adobe == null) && !isStream) {
        adobe = new AdobeMarkerSegment(transform);
        insertAdobeMarkerSegment(adobe);
      } else {
        adobe.transform = transform;
      }
    } else if (adobe != null) {
      markerSequence.remove(adobe);
    }

    boolean updateQtables = false;
    boolean updateHtables = false;

    boolean progressive = false;

    int[] subsampledSelectors = {0, 1, 1, 0};
    int[] nonSubsampledSelectors = {0, 0, 0, 0};

    int[] newTableSelectors = willSubsample
        ? subsampledSelectors
        : nonSubsampledSelectors;

    // Keep the old componentSpecs array
    SOFMarkerSegment.ComponentSpec[] oldCompSpecs = null;
    // SOF might be modified
    if (sof != null) {
      oldCompSpecs = sof.componentSpecs;
      progressive = (sof.tag == JPEG.SOF2);
      // Now replace the SOF with a new one; it might be the same, but
      // this is easier.
      markerSequence.set(markerSequence.indexOf(sof),
          new SOFMarkerSegment(progressive,
              false, // we never need extended
              willSubsample,
              ids,
              numChannels));

      // Now suss out if subsampling changed and set the boolean for
      // updating the q tables
      // if the old componentSpec q table selectors don't match
      // the new ones, update the qtables.  The new selectors are already
      // in place in the new SOF segment above.
      for (int i = 0; i < oldCompSpecs.length; i++) {
        if (oldCompSpecs[i].QtableSelector != newTableSelectors[i]) {
          updateQtables = true;
        }
      }

      if (progressive) {
        // if the component ids are different, update all the existing scans
        // ignore Huffman tables
        boolean idsDiffer = false;
        for (int i = 0; i < oldCompSpecs.length; i++) {
          if (ids[i] != oldCompSpecs[i].componentId) {
            idsDiffer = true;
          }
        }
        if (idsDiffer) {
          // update the ids in each SOS marker segment
          for (Iterator iter = markerSequence.iterator(); iter.hasNext(); ) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            if (seg instanceof SOSMarkerSegment) {
              SOSMarkerSegment target = (SOSMarkerSegment) seg;
              for (int i = 0; i < target.componentSpecs.length; i++) {
                int oldSelector =
                    target.componentSpecs[i].componentSelector;
                // Find the position in the old componentSpecs array
                // of the old component with the old selector
                // and replace the component selector with the
                // new id at the same position, as these match
                // the new component specs array in the SOF created
                // above.
                for (int j = 0; j < oldCompSpecs.length; j++) {
                  if (oldCompSpecs[j].componentId == oldSelector) {
                    target.componentSpecs[i].componentSelector =
                        ids[j];
                  }
                }
              }
            }
          }
        }
      } else {
        if (sos != null) {
          // htables - if the old htable selectors don't match the new ones,
          // update the tables.
          for (int i = 0; i < sos.componentSpecs.length; i++) {
            if ((sos.componentSpecs[i].dcHuffTable
                != newTableSelectors[i])
                || (sos.componentSpecs[i].acHuffTable
                != newTableSelectors[i])) {
              updateHtables = true;
            }
          }

          // Might be the same as the old one, but this is easier.
          markerSequence.set(markerSequence.indexOf(sos),
              new SOSMarkerSegment(willSubsample,
                  ids,
                  numChannels));
        }
      }
    } else {
      // should be stream metadata if there isn't an SOF, but check it anyway
      if (isStream) {
        // update tables - routines below check if it's really necessary
        updateQtables = true;
        updateHtables = true;
      }
    }

    if (updateQtables) {
      List tableSegments = new ArrayList();
      for (Iterator iter = markerSequence.iterator(); iter.hasNext(); ) {
        MarkerSegment seg = (MarkerSegment) iter.next();
        if (seg instanceof DQTMarkerSegment) {
          tableSegments.add(seg);
        }
      }
      // If there are no tables, don't add them, as the metadata encodes an
      // abbreviated stream.
      // If we are not subsampling, we just need one, so don't do anything
      if (!tableSegments.isEmpty() && willSubsample) {
        // Is it really necessary?  There should be at least 2 tables.
        // If there is only one, assume it's a scaled "standard"
        // luminance table, extract the scaling factor, and generate a
        // scaled "standard" chrominance table.

        // Find the table with selector 1.
        boolean found = false;
        for (Iterator iter = tableSegments.iterator(); iter.hasNext(); ) {
          DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next();
          for (Iterator tabiter = testdqt.tables.iterator();
              tabiter.hasNext(); ) {
            DQTMarkerSegment.Qtable tab =
                (DQTMarkerSegment.Qtable) tabiter.next();
            if (tab.tableID == 1) {
              found = true;
            }
          }
        }
        if (!found) {
          //    find the table with selector 0.  There should be one.
          DQTMarkerSegment.Qtable table0 = null;
          for (Iterator iter = tableSegments.iterator(); iter.hasNext(); ) {
            DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next();
            for (Iterator tabiter = testdqt.tables.iterator();
                tabiter.hasNext(); ) {
              DQTMarkerSegment.Qtable tab =
                  (DQTMarkerSegment.Qtable) tabiter.next();
              if (tab.tableID == 0) {
                table0 = tab;
              }
            }
          }

          // Assuming that the table with id 0 is a luminance table,
          // compute a new chrominance table of the same quality and
          // add it to the last DQT segment
          DQTMarkerSegment dqt =
              (DQTMarkerSegment) tableSegments.get(tableSegments.size() - 1);
          dqt.tables.add(dqt.getChromaForLuma(table0));
        }
      }
    }

    if (updateHtables) {
      List tableSegments = new ArrayList();
      for (Iterator iter = markerSequence.iterator(); iter.hasNext(); ) {
        MarkerSegment seg = (MarkerSegment) iter.next();
        if (seg instanceof DHTMarkerSegment) {
          tableSegments.add(seg);
        }
      }
      // If there are no tables, don't add them, as the metadata encodes an
      // abbreviated stream.
      // If we are not subsampling, we just need one, so don't do anything
      if (!tableSegments.isEmpty() && willSubsample) {
        // Is it really necessary?  There should be at least 2 dc and 2 ac
        // tables.  If there is only one, add a
        // "standard " chrominance table.

        // find a table with selector 1. AC/DC is irrelevant
        boolean found = false;
        for (Iterator iter = tableSegments.iterator(); iter.hasNext(); ) {
          DHTMarkerSegment testdht = (DHTMarkerSegment) iter.next();
          for (Iterator tabiter = testdht.tables.iterator();
              tabiter.hasNext(); ) {
            DHTMarkerSegment.Htable tab =
                (DHTMarkerSegment.Htable) tabiter.next();
            if (tab.tableID == 1) {
              found = true;
            }
          }
        }
        if (!found) {
          // Create new standard dc and ac chrominance tables and add them
          // to the last DHT segment
          DHTMarkerSegment lastDHT =
              (DHTMarkerSegment) tableSegments.get(tableSegments.size() - 1);
          lastDHT.addHtable(JPEGHuffmanTable.StdDCLuminance, true, 1);
          lastDHT.addHtable(JPEGHuffmanTable.StdACLuminance, true, 1);
        }
      }
    }
  }

  private boolean wantAlpha(Node transparency) {
    boolean returnValue = false;
    Node alpha = transparency.getFirstChild();  // Alpha must be first if present
    if (alpha.getNodeName().equals("Alpha")) {
      if (alpha.hasAttributes()) {
        String value =
            alpha.getAttributes().getNamedItem("value").getNodeValue();
        if (!value.equals("none")) {
          returnValue = true;
        }
      }
    }
    transparencyDone = true;
    return returnValue;
  }

  private void mergeStandardCompressionNode(Node node)
      throws IIOInvalidTreeException {
    // NumProgressiveScans is ignored.  Progression must be enabled on the
    // ImageWriteParam.
    // No-op
  }

  private void mergeStandardDataNode(Node node)
      throws IIOInvalidTreeException {
    // No-op
  }

  private void mergeStandardDimensionNode(Node node)
      throws IIOInvalidTreeException {
    // Pixel Aspect Ratio or pixel size can be incorporated if there is,
    // or can be, a JFIF segment
    JFIFMarkerSegment jfif =
        (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
    if (jfif == null) {
      // Can there be one?
      // Criteria:
      // SOF must be present with 1 or 3 channels, (stream metadata fails this)
      //     Component ids must be JFIF compatible.
      boolean canHaveJFIF = false;
      SOFMarkerSegment sof =
          (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true);
      if (sof != null) {
        int numChannels = sof.componentSpecs.length;
        if ((numChannels == 1) || (numChannels == 3)) {
          canHaveJFIF = true; // remaining tests are negative
          for (int i = 0; i < sof.componentSpecs.length; i++) {
            if (sof.componentSpecs[i].componentId != i + 1) {
              canHaveJFIF = false;
            }
          }
          // if Adobe present, transform = ADOBE_UNKNOWN for 1-channel,
          //     ADOBE_YCC for 3-channel.
          AdobeMarkerSegment adobe =
              (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class,
                  true);
          if (adobe != null) {
            if (adobe.transform != ((numChannels == 1)
                ? JPEG.ADOBE_UNKNOWN
                : JPEG.ADOBE_YCC)) {
              canHaveJFIF = false;
            }
          }
        }
      }
      // If so, create one and insert it into the sequence.  Note that
      // default is just pixel ratio at 1:1
      if (canHaveJFIF) {
        jfif = new JFIFMarkerSegment();
        markerSequence.add(0, jfif);
      }
    }
    if (jfif != null) {
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        NamedNodeMap attrs = child.getAttributes();
        String name = child.getNodeName();
        if (name.equals("PixelAspectRatio")) {
          String valueString = attrs.getNamedItem("value").getNodeValue();
          float value = Float.parseFloat(valueString);
          Point p = findIntegerRatio(value);
          jfif.resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
          jfif.Xdensity = p.x;
          jfif.Xdensity = p.y;
        } else if (name.equals("HorizontalPixelSize")) {
          String valueString = attrs.getNamedItem("value").getNodeValue();
          float value = Float.parseFloat(valueString);
          // Convert from mm/dot to dots/cm
          int dpcm = (int) Math.round(1.0 / (value * 10.0));
          jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM;
          jfif.Xdensity = dpcm;
        } else if (name.equals("VerticalPixelSize")) {
          String valueString = attrs.getNamedItem("value").getNodeValue();
          float value = Float.parseFloat(valueString);
          // Convert from mm/dot to dots/cm
          int dpcm = (int) Math.round(1.0 / (value * 10.0));
          jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM;
          jfif.Ydensity = dpcm;
        }

      }
    }
  }

  /*
   * Return a pair of integers whose ratio (x/y) approximates the given
   * float value.
   */
  private static Point findIntegerRatio(float value) {
    float epsilon = 0.005F;

    // Normalize
    value = Math.abs(value);

    // Deal with min case
    if (value <= epsilon) {
      return new Point(1, 255);
    }

    // Deal with max case
    if (value >= 255) {
      return new Point(255, 1);
    }

    // Remember if we invert
    boolean inverted = false;
    if (value < 1.0) {
      value = 1.0F / value;
      inverted = true;
    }

    // First approximation
    int y = 1;
    int x = (int) Math.round(value);

    float ratio = (float) x;
    float delta = Math.abs(value - ratio);
    while (delta > epsilon) { // not close enough
      // Increment y and compute a new x
      y++;
      x = (int) Math.round(y * value);
      ratio = (float) x / (float) y;
      delta = Math.abs(value - ratio);
    }
    return inverted ? new Point(y, x) : new Point(x, y);
  }

  private void mergeStandardDocumentNode(Node node)
      throws IIOInvalidTreeException {
    // No-op
  }

  private void mergeStandardTextNode(Node node)
      throws IIOInvalidTreeException {
    // Convert to comments.  For the moment ignore the encoding issue.
    // Ignore keywords, language, and encoding (for the moment).
    // If compression tag is present, use only entries with "none".
    NodeList children = node.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node child = children.item(i);
      NamedNodeMap attrs = child.getAttributes();
      Node comp = attrs.getNamedItem("compression");
      boolean copyIt = true;
      if (comp != null) {
        String compString = comp.getNodeValue();
        if (!compString.equals("none")) {
          copyIt = false;
        }
      }
      if (copyIt) {
        String value = attrs.getNamedItem("value").getNodeValue();
        COMMarkerSegment com = new COMMarkerSegment(value);
        insertCOMMarkerSegment(com);
      }
    }
  }

  private void mergeStandardTransparencyNode(Node node)
      throws IIOInvalidTreeException {
    // This might indicate that an alpha channel is being added or removed.
    // The nodes must appear in order, and a Chroma node will process any
    // transparency, so process it here only if there was no Chroma node
    // Do nothing for stream metadata
    if (!transparencyDone && !isStream) {
      boolean wantAlpha = wantAlpha(node);
      // do we have alpha already?  If the number of channels is 2 or 4,
      // we do, as we don't support CMYK, nor can we add alpha to it
      // The number of channels can be determined from the SOF
      JFIFMarkerSegment jfif = (JFIFMarkerSegment) findMarkerSegment
          (JFIFMarkerSegment.class, true);
      AdobeMarkerSegment adobe = (AdobeMarkerSegment) findMarkerSegment
          (AdobeMarkerSegment.class, true);
      SOFMarkerSegment sof = (SOFMarkerSegment) findMarkerSegment
          (SOFMarkerSegment.class, true);
      SOSMarkerSegment sos = (SOSMarkerSegment) findMarkerSegment
          (SOSMarkerSegment.class, true);

      // We can do nothing for progressive, as we don't know how to
      // modify the scans.
      if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive
        return;
      }

      // Do we already have alpha?  We can tell by the number of channels
      // We must have an sof, or we can't do anything further
      if (sof != null) {
        int numChannels = sof.componentSpecs.length;
        boolean hadAlpha = (numChannels == 2) || (numChannels == 4);
        // proceed only if the old state and the new state differ
        if (hadAlpha != wantAlpha) {
          if (wantAlpha) {  // Adding alpha
            numChannels++;
            if (jfif != null) {
              markerSequence.remove(jfif);
            }

            // If an adobe marker is present, transform must be UNKNOWN
            if (adobe != null) {
              adobe.transform = JPEG.ADOBE_UNKNOWN;
            }

            // Add a component spec with appropriate parameters to SOF
            SOFMarkerSegment.ComponentSpec[] newSpecs =
                new SOFMarkerSegment.ComponentSpec[numChannels];
            for (int i = 0; i < sof.componentSpecs.length; i++) {
              newSpecs[i] = sof.componentSpecs[i];
            }
            byte oldFirstID = (byte) sof.componentSpecs[0].componentId;
            byte newID = (byte) ((oldFirstID > 1) ? 'A' : 4);
            newSpecs[numChannels - 1] =
                sof.getComponentSpec(newID,
                    sof.componentSpecs[0].HsamplingFactor,
                    sof.componentSpecs[0].QtableSelector);

            sof.componentSpecs = newSpecs;

            // Add a component spec with appropriate parameters to SOS
            SOSMarkerSegment.ScanComponentSpec[] newScanSpecs =
                new SOSMarkerSegment.ScanComponentSpec[numChannels];
            for (int i = 0; i < sos.componentSpecs.length; i++) {
              newScanSpecs[i] = sos.componentSpecs[i];
            }
            newScanSpecs[numChannels - 1] =
                sos.getScanComponentSpec(newID, 0);
            sos.componentSpecs = newScanSpecs;
          } else {  // Removing alpha
            numChannels--;
            // Remove a component spec from SOF
            SOFMarkerSegment.ComponentSpec[] newSpecs =
                new SOFMarkerSegment.ComponentSpec[numChannels];
            for (int i = 0; i < numChannels; i++) {
              newSpecs[i] = sof.componentSpecs[i];
            }
            sof.componentSpecs = newSpecs;

            // Remove a component spec from SOS
            SOSMarkerSegment.ScanComponentSpec[] newScanSpecs =
                new SOSMarkerSegment.ScanComponentSpec[numChannels];
            for (int i = 0; i < numChannels; i++) {
              newScanSpecs[i] = sos.componentSpecs[i];
            }
            sos.componentSpecs = newScanSpecs;
          }
        }
      }
    }
  }


  public void setFromTree(String formatName, Node root)
      throws IIOInvalidTreeException {
    if (formatName == null) {
      throw new IllegalArgumentException("null formatName!");
    }
    if (root == null) {
      throw new IllegalArgumentException("null root!");
    }
    if (isStream &&
        (formatName.equals(JPEG.nativeStreamMetadataFormatName))) {
      setFromNativeTree(root);
    } else if (!isStream &&
        (formatName.equals(JPEG.nativeImageMetadataFormatName))) {
      setFromNativeTree(root);
    } else if (!isStream &&
        (formatName.equals
            (IIOMetadataFormatImpl.standardMetadataFormatName))) {
      // In this case a reset followed by a merge is correct
      super.setFromTree(formatName, root);
    } else {
      throw new IllegalArgumentException("Unsupported format name: "
          + formatName);
    }
  }

  private void setFromNativeTree(Node root) throws IIOInvalidTreeException {
    if (resetSequence == null) {
      resetSequence = markerSequence;
    }
    markerSequence = new ArrayList();

    // Build a whole new marker sequence from the tree

    String name = root.getNodeName();
    if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName
        : JPEG.nativeImageMetadataFormatName)) {
      throw new IIOInvalidTreeException("Invalid root node name: " + name,
          root);
    }
    if (!isStream) {
      if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence
        throw new IIOInvalidTreeException(
            "JPEGvariety and markerSequence nodes must be present", root);
      }

      Node JPEGvariety = root.getFirstChild();

      if (JPEGvariety.getChildNodes().getLength() != 0) {
        markerSequence.add(new JFIFMarkerSegment(JPEGvariety.getFirstChild()));
      }
    }

    Node markerSequenceNode = isStream ? root : root.getLastChild();
    setFromMarkerSequenceNode(markerSequenceNode);

  }

  void setFromMarkerSequenceNode(Node markerSequenceNode)
      throws IIOInvalidTreeException {

    NodeList children = markerSequenceNode.getChildNodes();
    // for all the children, add a marker segment
    for (int i = 0; i < children.getLength(); i++) {
      Node node = children.item(i);
      String childName = node.getNodeName();
      if (childName.equals("dqt")) {
        markerSequence.add(new DQTMarkerSegment(node));
      } else if (childName.equals("dht")) {
        markerSequence.add(new DHTMarkerSegment(node));
      } else if (childName.equals("dri")) {
        markerSequence.add(new DRIMarkerSegment(node));
      } else if (childName.equals("com")) {
        markerSequence.add(new COMMarkerSegment(node));
      } else if (childName.equals("app14Adobe")) {
        markerSequence.add(new AdobeMarkerSegment(node));
      } else if (childName.equals("unknown")) {
        markerSequence.add(new MarkerSegment(node));
      } else if (childName.equals("sof")) {
        markerSequence.add(new SOFMarkerSegment(node));
      } else if (childName.equals("sos")) {
        markerSequence.add(new SOSMarkerSegment(node));
      } else {
        throw new IIOInvalidTreeException("Invalid "
            + (isStream ? "stream " : "image ") + "child: "
            + childName, node);
      }
    }
  }

  /**
   * Check that this metadata object is in a consistent state and
   * return <code>true</code> if it is or <code>false</code>
   * otherwise.  All the constructors and modifiers should call
   * this method at the end to guarantee that the data is always
   * consistent, as the writer relies on this.
   */
  private boolean isConsistent() {
    SOFMarkerSegment sof =
        (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class,
            true);
    JFIFMarkerSegment jfif =
        (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class,
            true);
    AdobeMarkerSegment adobe =
        (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class,
            true);
    boolean retval = true;
    if (!isStream) {
      if (sof != null) {
        // SOF numBands = total scan bands
        int numSOFBands = sof.componentSpecs.length;
        int numScanBands = countScanBands();
        if (numScanBands != 0) {  // No SOS is OK
          if (numScanBands != numSOFBands) {
            retval = false;
          }
        }
        // If JFIF is present, component ids are 1-3, bands are 1 or 3
        if (jfif != null) {
          if ((numSOFBands != 1) && (numSOFBands != 3)) {
            retval = false;
          }
          for (int i = 0; i < numSOFBands; i++) {
            if (sof.componentSpecs[i].componentId != i + 1) {
              retval = false;
            }
          }

          // If both JFIF and Adobe are present,
          // Adobe transform == unknown for gray,
          // YCC for 3-chan.
          if ((adobe != null)
              && (((numSOFBands == 1)
              && (adobe.transform != JPEG.ADOBE_UNKNOWN))
              || ((numSOFBands == 3)
              && (adobe.transform != JPEG.ADOBE_YCC)))) {
            retval = false;
          }
        }
      } else {
        // stream can't have jfif, adobe, sof, or sos
        SOSMarkerSegment sos =
            (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class,
                true);
        if ((jfif != null) || (adobe != null)
            || (sof != null) || (sos != null)) {
          retval = false;
        }
      }
    }
    return retval;
  }

  /**
   * Returns the total number of bands referenced in all SOS marker
   * segments, including 0 if there are no SOS marker segments.
   */
  private int countScanBands() {
    List ids = new ArrayList();
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof SOSMarkerSegment) {
        SOSMarkerSegment sos = (SOSMarkerSegment) seg;
        SOSMarkerSegment.ScanComponentSpec[] specs = sos.componentSpecs;
        for (int i = 0; i < specs.length; i++) {
          Integer id = new Integer(specs[i].componentSelector);
          if (!ids.contains(id)) {
            ids.add(id);
          }
        }
      }
    }

    return ids.size();
  }

  ///// Writer support

  void writeToStream(ImageOutputStream ios,
      boolean ignoreJFIF,
      boolean forceJFIF,
      List thumbnails,
      ICC_Profile iccProfile,
      boolean ignoreAdobe,
      int newAdobeTransform,
      JPEGImageWriter writer)
      throws IOException {
    if (forceJFIF) {
      // Write a default JFIF segment, including thumbnails
      // This won't be duplicated below because forceJFIF will be
      // set only if there is no JFIF present already.
      JFIFMarkerSegment.writeDefaultJFIF(ios,
          thumbnails,
          iccProfile,
          writer);
      if ((ignoreAdobe == false)
          && (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE)) {
        if ((newAdobeTransform != JPEG.ADOBE_UNKNOWN)
            && (newAdobeTransform != JPEG.ADOBE_YCC)) {
          // Not compatible, so ignore Adobe.
          ignoreAdobe = true;
          writer.warningOccurred
              (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB);
        }
      }
    }
    // Iterate over each MarkerSegment
    Iterator iter = markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof JFIFMarkerSegment) {
        if (ignoreJFIF == false) {
          JFIFMarkerSegment jfif = (JFIFMarkerSegment) seg;
          jfif.writeWithThumbs(ios, thumbnails, writer);
          if (iccProfile != null) {
            JFIFMarkerSegment.writeICC(iccProfile, ios);
          }
        } // Otherwise ignore it, as requested
      } else if (seg instanceof AdobeMarkerSegment) {
        if (ignoreAdobe == false) {
          if (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE) {
            AdobeMarkerSegment newAdobe =
                (AdobeMarkerSegment) seg.clone();
            newAdobe.transform = newAdobeTransform;
            newAdobe.write(ios);
          } else if (forceJFIF) {
            // If adobe isn't JFIF compatible, ignore it
            AdobeMarkerSegment adobe = (AdobeMarkerSegment) seg;
            if ((adobe.transform == JPEG.ADOBE_UNKNOWN)
                || (adobe.transform == JPEG.ADOBE_YCC)) {
              adobe.write(ios);
            } else {
              writer.warningOccurred
                  (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB);
            }
          } else {
            seg.write(ios);
          }
        } // Otherwise ignore it, as requested
      } else {
        seg.write(ios);
      }
    }
  }

  //// End of writer support

  public void reset() {
    if (resetSequence != null) {  // Otherwise no need to reset
      markerSequence = resetSequence;
      resetSequence = null;
    }
  }

  public void print() {
    for (int i = 0; i < markerSequence.size(); i++) {
      MarkerSegment seg = (MarkerSegment) markerSequence.get(i);
      seg.print();
    }
  }

}
